El R permite diferentes tipos de análisis de datos aplicados a textos. Según las características de cada obra, podemos llevar a cabo un análisis de frecuencia de palabras, de sentimientos, emplear diccionarios (léxicos o glosarios) para identificar ciertos rasgos o atributos. También resulta posible jugar con su estructura, agregando los datos por párrafo, capítulo, personaje o cualquier otra características que lo permita.
Para el presente curso, hemos seleccionado dos textos literarios: una novela y una obra de teatro. La novela es “La Regenta”, de Leopoldo Alas “Clarín” y la pieza de teatro “Tres sombreros de copa”, de Miguel Mihura. Ambas obras son clásicos de la literatura española y permiten comparar los resultados de los análisis de textos literarios. Además de por su alto valor literario, nos interesan en particular por sus diferencias estructurales y cómo nos permiten realizar distintos tipos de análisis.
Aunque ambas puedan ser objeto de la mayor parte de los análisis que introduciremos aquí, cada una de ellas nos permitirá aplicar distintas técnicas de modo más fructífero. Por ejemplo, en “La Regenta” nos interesará explorar la estructura de los capítulos, los temas que aparecen, sus principales personajes, la frecuencia de palabras y la red de palabras. En el caso de “Tres sombreros de copa”, nos interesará explorar la red de diálogos y medir las diferentes formas de centralidad o influencia de los personajes en la trama.
“La Regenta”: párrafos y capítulos
Estructura de la base de datos
La novela “La Regenta” es una obra de Leopoldo Alas “Clarín” publicada en 1884. Como sabéis, se trata de una de las novelas más importantes de la literatura española y representa uno de los mejores ejemplares de la novela realista/naturalista del siglo XIX. Se conforma por 30 capítulos, divididos en dos tomos de 15 apartados cada uno.
El objetivo de esta presente sección consiste en preparar el texto de “La Regenta” para su análisis por medio de herramientas y técnicas estadísticas. De modo concreto, nos interesa organizar el texto en dos bases de datos. La primera organizada por párrafos y la segunda por capítulos. Cada una de ellas permitirá la aplicación de análisis con distinto nivel de detalle.
Llamaremos aquí “base de datos” una tabla de datos con N filas y N columnas. Cada fila corresponderá a un párrafo o capítulo y cada columna a una variable que nos interese analizar. Tendremos, por lo tanto, dos unidades de agregación y sus correspondientes atributos: parte (título, prólogo o tomo), capítulo, párrafo (en su caso) y texto.
Método de conversión de texto a datos
Resulta muy sencillo descargar la novela “La Regenta”. Podemos emplear el paquete gutenbergr para descargar el texto directamente en R y trabajar con él. Solo tenemos que añadir un paso más al trabajo: convertir la codificación de caracteres a “latin1” para evitar problemas con las tildes.
Código
# Abre la librería gutenbergr# para bajar el textolibrary(gutenbergr)# Baja el texto de "La Regenta"# cuyo id es igual 17073re <-gutenberg_download(gutenberg_id =17073, verbose =FALSE)# Cambia la codificación de caracteres a# a latin1 para evitar problemas con las tildesEncoding(re$text) <-"latin1"# Convierte el texto en un solo stringre <-paste0(re$text, collapse ="\n")
Con ese sencillo primer paso, ya tenemos la novela en nuestro ordenador. No obstante, hacen falta algunos tratamientos adicionales para convertir el texto en una base de datos que se pueda emplear en comparaciones, análisis de redes de palabras, etc.
La etapa siguiente trata de eliminar los saltos de línea y recuperar la estructura de los párrafos. Para ello, empleamos una expresión regular que nos permita identificar los párrafos y corregir los saltos de línea al final de cada frase.
Código
# Carga el paquete stringi# que permite trabajar con expresiones regulares# y otras tareas de manipulación# de textoslibrary(stringi)# Corrige los párrafos para que no tengan# un salto de línea al final de cada fraserx <-stri_replace_all_regex( re,"(\\S|\\p{L})(\n)(\\S{1}|\\p{L})","$1 $3")
Para entender lo que hemos hecho, explicaremos la gramática de la expresión regular empleada:
(\\S|\\p{L}): busca uno o más caracteres que no sean espacios en blanco (\S) o (|) una letra con acentuación latina (\p{L}).
(\n): busca un salto de línea.
(\\S{1}|\\p{L}): busca un único carácter que no sea un espacio en blanco (\S{1}) o (|) una letra con acentuación latina (\p{L}).
y los reemplaza por:
$1 $3: por el primer carácter encontrado ($1), un espacio y el último carácter encontrado ($3).
De ese modo:
“El poeta es un fingidor.”
“Finge tan completamente”
“que llega a fingir que es dolor”
“el dolor que de verdad siente.”
se convierte en:
“El poeta es un fingidor. Finge tan completamente que llega a fingir que es dolor el dolor que de verdad siente.”
De ese modo, el R lo hace para TODOS los párrafos a la vez y nos ahora mucho trabajo de preparación de cualquier texto.
Una vez corregidos los párrafos, podemos convertir el texto en un vector de líneas para poder trabajar con él.
Código
# Carga el paquete readr, que permite# convertir textos en vectoreslibrary(readr)# Vuelve a seleccionar los párrafosre <-read_lines(rx)
El resultado es un vector de caracteres llamado re con 12.164 elementos. Cada elemento corresponde o bien a un párrafo o bien a un salto de línea indicando la separación entre dos párrafos.
Con esa información, nos interesa asociar cada párrafo a la estructura formal de la novela. En el caso de “La Regenta”, la novela está dividida en un prólogo, dos tomos y 30 capítulos, siendo los quince primeros pertenecientes al primer tomo y los quince restantes al segundo.
Para llevar a cabo dicha tarea, debemos identificar los elementos que marcan el inicio de cada parte de la novela. En este caso, el prólogo, los tomos y los capítulos. El prólogo y los tomos son fácilmente identificables por su título. Basta con buscar en qué líneas aparecen las palabras “Prólogo” y “Tomo” para identificarlos.
Los capítulos, por otra parte, exigen un poco más de trabajo. Se enumeran con números romanos precedidos y seguidos de dos guiones. Por ejemplo, “–I–” indica el punto en el que empieza primer capítulo. “–II–” encuentra el elemento que da inicio al segundo y así sucesivamente. En nuestro ejemplo, el primer capítulo empieza en la línea 59 y el segundo en la 311. Por lo tanto, sabemos que todos los párrafos comprendidos entre 59 y 310 corresponden al primer capítulo. Con esa información en mano, lo que tenemos que hacer es asociar a cada párrafo su correspondiente título, tomo o capítulo.
El primer procedimiento consiste en encontrar los índices de las partes y capítulos de la novela y sus descripciones:
Código
# Encuentra los índices del prólogo, de# los tomos y los capítulospro <-grep("Prólogo",re) # prólogotm <-c(grep("Tomo",re), grep("TOMO",re)) # tomos - índicecap <-which(stri_detect_regex(re, "^(--)([A-Z]+)(--)$")==TRUE) # capítulos# Crea vectores que obtienen# los títulos de los tomos y los capítulostx <- re[tm] # tomos - textoscx <- re[cap] # capítulos - textos
Con los índices, puedo repetir cada nombre de tomo, capítulo, prólogo, etc. en función de cuántos párrafos tenga cada uno. De ese modo, puedo asociar cada párrafo a su correspondiente título, tomo o capítulo:
Código
# Genera un vector que identifica qué líneas# pertenecen al título. En la estructura de# la novela el prólogo se sigue al título,# por eso decimos que se repita la palabra# "Título" del primer párrafo hasta el# inmediatamente anterior al prólogo (pro-1).ti <-rep("Título", length(1:(pro-1)))# Hacemos algo parecido con el prólogo. # Puesto que antecede al primer tomo,# repetimos "Prólogo" desde la primera # vez que aparece (pro) hasta el párrafo# anterior al primer tomo (tm[1]-1).pro <-rep("Prólogo", length(pro:(tm[1]-1)))# Para los tomos# Encuentra el tamaño en párrafos# de cada tomolen <-diff(c(tm, length(re)+1))# Repite la descripción o el nombre# de cada tomo para identificar# cada párrafota <-sapply(1:(length(tx)), function(i){rep(tx[i], len[i]) }, simplify =TRUE)ta <-unlist(ta)# Para los capítulos# Encuentra el tamaño en párrafos# de cada capítulolen <-diff(c(cap, length(re)+1))# Repite la descripción o el nombre# de cada capítulo para identificar# cada párrafoca <-sapply(1:(length(cx)), function(i){rep(cx[i], len[i]) }, simplify =TRUE)ca <-unlist(ca)
Finalmente, empleo todos los vectores generados para crear una base de datos que refleje de modo correcto la parte, el capítulo y el texto de cada párrafo:
Código
# Crea un data frame con los textos, la # identificación, de la parte y del capítulo# Combina los vectores de título,# prólogo, tomos en un vetor pt (parte)pt <-c(ti,pro,ta)# Añade "Previa" para identificar # aquellos párrafos que pertenecen # al título, prólogo y presentación # del primer tomo y combina con los # capítuloscp <-c(rep("Previa", 58), ca)# Genera una base de datos con las# informaciones completas de# identificación de las partes,# capítulos y el texto.dx <-data.frame(parte = pt, capitulo= cp, texto = re)
Ahora, nos toca transformar la numeración de los capítulos para poder mantener un orden secuencial. Además, nos interesa saber el número del párrafo en cada capítulo, para poder mencionar exactamente dónde se encuentra una referencia exacta en el texto. También queremos eliminar la información que no nos interesa, como los espacios en blanco entre párrafos:
Código
# Elimina los guiones de al identificación de# cada capítulodx$capitulo <-gsub("--", "", dx$capitulo)# Convierte los textos de identificación de los# capítulos en números romanos y luego los# convierte en numéricodx$roman <-as.roman(dx$capitulo)dx$roman <-as.numeric(dx$roman)# Asigna los valores numéricos a los capítulosdx$capitulo[!is.na(dx$roman)] <- dx$roman[!is.na(dx$roman)]dx$capitulo[is.na(dx$roman)] <- dx$parte[is.na(dx$roman)]# Añade un 0 (cero) para los capítulos menores a# 10.dx$capitulo[nchar(dx$capitulo) ==1] <-paste0("0", dx$capitulo[nchar(dx$capitulo) ==1])# Añade 001 y 002 para el título y el prólogo.# De ese modo, quedan los primeros una vez se# ordene la base de datos.dx$capitulo[dx$capitulo =="Título"] <-"001 - Título"dx$capitulo[dx$capitulo =="Prólogo"] <-"002 - Prólogo"# Elimina la información que no interesadx <- dx[dx$parte != dx$texto, ]dx$roman <-NULLdx <- dx[dx$texto !="", ]# Añade un número de párrafo a cada párrafo de# cada capítulo. Así que siempre se reinicia en# cuenteo a cada nuevo capítulo.library(dplyr)dx <- dx |>group_by(capitulo) |>mutate(parrafo =row_number())# Elimina los espacios en blanco al principio y# al final del textodx$texto <-trimws(dx$texto)# Selecciona solo las variables.de interésdx <- dx[, c("parte", "capitulo", "parrafo", "texto")]# Visualiza los resultadoslibrary(reactable)reactable(dx, resizable = T, wrap = F)
El paso siguiente consiste en crear una versión distinta del mismo texto. Pero ahora, la novela será dividida de forma que cada observación en la base de datos corresponderá a un capítulo completo:
Código
# Genera una base de datos agregada por capítulo# (y no por párrafo, como la anterior)dc <-aggregate(list(texto = dx$texto), by =list(parte = dx$parte,capitulo = dx$capitulo), FUN = paste, collapse ="\n\n")# Visualiza los resultadosreactable(dc, resizable = T, wrap = F)
Finalmente, estandarizamos los nombres de las dos bases de datos y, a continuación, las guardamos en un archivo de R que será empleado en los análisis:
Código
# Estandariza los nombres de las# bases de datosregp <- dxregc <- dc# Guarda los resultados# Elegid una ubicación en vuestro# ordenador donde podáis rescatar# los datos luego:# "C:/FiloR/Regenta.RData", por ejemplosave(regp, regc, file="textos/Regenta.RData")
¿Por qué estandarizamos los nombres? Una de las grandes ventajas de utilizar R se encuentra en su facilidad de manejo de diversas bases de datos de forma simultánea. Por ello, es importante que los nombres de las bases de datos sean fáciles de recordar y de escribir. Además, es importante que los nombres sean descriptivos, para que se pueda recordar fácilmente qué contiene cada base de datos. Cuando trabajemos más tarde con esas bases, sabremos que regp contiene los párrafos de “La Regenta” y que regc contiene los capítulos de “La Regenta”. Haremos algo semejante para la obra teatral.
Código como modelo
El código anterior es un modelo que puede ser empleado para el tratamiento de cualquier texto con una estructura semejante a de una novela. Para ello, solo es necesario cambiar la dirección del texto original y ajustar los nombres de las partes y capítulos. Además, es posible modificar el código para que se ajuste a las necesidades de cada texto. Por ejemplo, si el texto original no tiene partes, se puede eliminar el segmento de código responsable de la división en partes. O si el texto no tiene capítulos, se puede eliminar la sección relativa a la división en capítulos.
No hay un código único que sea válido para todos los textos. Por ello, es necesario adaptar el código a las características de cada texto. Sin embargo, el código presentado aquí es un buen punto de partida para el tratamiento de cualquier texto con una estructura de partes o capítulos.
Más abajo aplicamos una variante del mismo código al “Don Quijote”, por ejemplo. Su código en el Projecto Gutenberg es 2000.
Tres sombreros de copa: red de diálogos
Estructura de la base de datos
En el caso de una obra de teatro, la estructura de la base de datos es diferente. En lugar de tomos y capítulos, tenemos actos y escenas. Además, en lugar de párrafos, tenemos diálogos. Existen marcadores claros que nos permiten identificar cada una de las partes.
Por su misma estructura, además, las obras de teatro son excelente material para la realización de determinados análisis como el análisis de redes sociales (SNA, en su sigla en inglés). También permiten otros tipos de agregación, como, por ejemplo, por personaje o por acto. De ese modo, es posible analizar las diferencias en términos de lenguaje, vocabulario o temas. Aunque se pueda hacer algo parecido con una novela, el proceso de identificación del diálogo de cada personaje resulta significativamente más laborioso cuando comparado con una pieza teatral.
En el caso de la obra de teatro, obtendremos tres bases de datos. La primera contendrá los datos del acto, personaje que habla, personaje a quien destina su habla, el orden del diálogo en la obra y el texto del diálogo.
La segunda y tercera base de datos estarán conformadas por una lista de vínculos entre pares de personajes y el número de veces que se comunican de forma dirigida y no dirigida.
Una red dirigida es aquella en la que se establece una relación de un nodo a otro que puede ser asimétrica. En este caso, la dirección importa. En este caso, el personaje A se dirige al personaje B. El primero es activo y el segundo pasivo y ni siempre existe una correspondencia perfecta o simétrica.
Sin embargo, en una red no dirigida, la relación es recíproca o la dirección indeterminada. No consideramos quién habla con quién, sino la intensidad de su vínculo o el total de veces que han interactuado.
Pensemos en un ejemplo claro extraído de las redes sociales. No es lo mismo seguir a Rosalía o cualquier persona famosa que ser seguido por una de ellas. En una red no dirigida, solo sabemos que existe un vínculo entre dos personas, pero no sabemos quién es el que sigue el otro. En una dirigida, tenemos una información vital que nos permite entender mejor la importancia de cada nodo en la red.
Cada una de esas formas de tratar el vínculo entre los personajes nos ofrece información distinta sobre el rol y la importancia de cada uno de ellos en la obra. Por lo tanto, nos interesa tener ambas redes para analizarlas y compararlas.
Para ello, emplearemos la obra “Tres sombreros de copa” de Miguel Mihura. Se trata de una pieza en tres actos con 18 personajes. No presenta una estructura compleja, lo que facilita el tratamiento de los datos.
De diálogos a datos
El primer paso es leer el texto de la obra. Aquí transformaremos los diálogos en datos que puedan, luego, ser sometidos a análisis. Empezamos por leer el texto de la obra. Para ello, emplearemos la función pdf_text del paquete pdftools. A continuación, eliminaremos los espacios en blanco múltiples entre palabras y las cabeceras.
Código
# Carga los paquetes necesarios para el# tratamiento de los textoslibrary(pdftools) # Lee el pdf original de la obralibrary(readr) # Lee las líneas del texto library(stringi) # Funciones de manipulación de texto# Lee el pdf de la obra de Mihurapd <-pdf_text("https://www.edu.xunta.gal/centros/cpilorenzobaleiron/system/files/u2/mihura__miguel_-_tres_sombreros_de_copa.pdf")# Elimina los espacios en blanco múltiples entre# palabraspx <-read_lines(pd)px <-trimws(px)px <-gsub("\\s+", " ", px, perl = T)# Elimina las cabeceras de las páginas y los# números de página del PDFnn <-which(px %in%c("3 sombreros de copa Miguel Mihura"))pd <- px[-c(nn -1, nn)]# Elimina el número de la última páginapd <- pd[-length(pd)]# Hace una modificación del texto para facilitar# el reconocimiento de uno de los personajespd <-stri_replace_all_fixed(pd, "EL ODIOSO SEÑOR,","\nEL ODIOSO SEÑOR.")
En el siguiente paso, toca separar los diálogos de los personajes. Emplearemos una expresión regular que identifica los nombres de los personajes y los separa de los diálogos creando un prefijo “PERSONA-” para identificar más fácilmente qué líneas corresponden al nombre del interlocutor.
En la pieza, los nombres de los personajes se identifican por una o varias palabras en mayúsculas que inician la frase seguidas de un punto, un espacio y, luego, son sucedidas por el texto. Por ejemplo, “DIONISIO. No. No veo nada.” o “DON ROSARIO. Parece usted tonto, don Dionisio.” nos informan un patrón claro. El objetivo es cortar el texto en dos partes: una con el nombre del personaje y otra con el diálogo.
Código
# Identifica las líneas que contienen los nombres# de los personajes y las separa de los diálogospx <-stri_replace_all_regex(pd,"^([[A-Z|\\p{Lu}]+\\s{1,1}[A-Z|\\p{Lu}]+]+)(\\.\\s{1,1})","\n\nPERSONA-$1$2\n\n")
Como en el caso de la novela, empleamos una expresión regular para identificar los nombres y separarlos de los textos.
^ indica que la expresión regular buscará todo que comience una línea con las características a continuación.
[A-Z|\\p{Lu}]+ busca una o más letras en mayúsculas que puedan contener textos con tilde.
\\s{1,1} busca un espacio en blanco.
[A-Z|\\p{Lu}]+ busca una o más letras en mayúsculas que puedan contener textos con tilde.
Como podéis ver, la segunda expresión se repite, pues los nombres, que están en mayúsculas en la edición elegida, pueden estar compuestos por más de una palabra. También vemos que todo ese conjunto se encuentra dentro de un grupo []+ que indica que puede haber una o más repeticiones de ese conjunto, es decir, varias palabras en mayúsculas separadas por un espacio.
El segundo grupo de la expresión regular, (\\.\\s{1,1}), busca un punto seguido seguido de un espacio en blanco.
Así que, en resumen, la expresión regular busca una o más palabras en mayúsculas que inicien una línea y estén seguidas de un punto y un espacio en blanco. Este es el patrón que buscamos identificar.
La segunda añade dos saltos de línea (\n\n) y la fórmula PERSONA- antes de repetir los valores encontrados ($1$2) y, luego, introducir otros dos saltos de línea (\n\n) para separar el nombre del personaje del texto del diálogo.
Nuestros ejemplos quedarían así:
Original: “DIONISIO. No. No veo nada.”
Modificado: “\n\nPERSONA-DIONISIO.\n\nNo. No veo nada.”
Original: “DON ROSARIO. Parece usted tonto, don Dionisio.”
Modificado: “\n\nPERSONA-DON ROSARIO.\n\nParece usted tonto, don Dionisio.”
Al ejecutar la función read_lines() justo en seguida, el nombre y el textos quedan separados.
En el código abajo realiza justamente dicha tarea y, además, elimina los saltos de línea que se encuentran en medio de los diálogos.
Código
# Elimina los saltos de línea en los diálogospx <-read_lines(px)pd <-paste(px, collapse ="\n")px <-stri_replace_all_regex(pd,"(\\S|\\p{L})(\n|\n\n)(\\S{1}|\\p{L})","$1 $3")pd <-read_lines(px)
Con esto resuelto, obtenemos la descripción y los índices de los actos y personajes. Se emplearán luego para la creación de la base de datos con los diálogos.
Código
# Identifica los actosna <-which(stri_detect_regex(pd, "^ACTO")==TRUE)ta <- pd[na]# Identifica los personajesnp <-which(stri_detect_regex(pd, "^PERSONA-")==TRUE)pp <- pd[np]
Como en “La Regenta”, se crean variables o vectores que repiten el título del acto y el nombre del personaje para cada diálogo.
Código
# Atribuye una sección inicial de preámbulo# para la presentación de personajes y# la descripción de la escena inicialpt <-rep("Preambulo",length(1:(na[1]-1)))# Para cada acto, se asigna el título del actofor(i in1:(length(na)-1)){ na[i+1]-na[i] pt <-c(pt,rep(ta[i],length((na[i]):(na[i+1]-1))))}# Identifica el último actopt <-c(pt, rep(ta[length(na)],length((na[length(na)]):length(pd))))# Atribuye una sección inicial de introducciónpe <-rep("Introducción",length(1:(np[1]-1)))# Para cada personaje, se asigna el nombre del personajefor(i in1:(length(np)-1)){ np[i+1]-np[i] pe <-c(pe,rep(pp[i],length((np[i]):(np[i+1]-1))))}pe <-c(pe, rep(pp[length(np)],length((np[length(np)]):length(pd))))
Con todas las variables a mano:
las juntamos en una base de datos;
eliminamos las líneas vacías y;
eliminamos diálogos que contienen el nombre del acto.
Código
# Convierte en una base de datosdd <-data.frame(pt, pe, pd)dd <- dd[dd$pd!="",]dd <- dd[dd$pe!=dd$pd,]
Para garantizar que podamos regresar en cualquier momento a la secuencia original de los diálogos, creamos una variable que ordene los diálogos en el orden en que aparecen en la obra. No obstante, a veces, existe más de una línea de diálogo por personaje. El orden debe llevar en cuenta esta peculiaridad.
Código
# Define un orden para los# diálogosdd$ord <-NAdd$px <-paste0(dd$pt," - ", dd$pe)# Para cada diálogofor(i in1:nrow(dd)){# Establece el orden del dialogo# de forma secuencial dd$ord[i] <- i# Si es el segundo diálogo o posteriorif(i>1){# Si el personaje es el mismoif(dd$px[i]==dd$px[i-1]){# Mantiene el mismo orden del# personaje anterior dd$ord[i] <- dd$ord[i-1]# Caso contrario }else{# Aumenta el orden del personaje# (orden del anterior más uno) dd$ord[i] <- dd$ord[i-1]+1 } }}
Estamos casi ya. Ahora, juntamos el texto de un mismo personaje, en el mismo orden dentro de un acto, en un solo diálogo. De esa manera, evitamos múltiples observaciones que, en realidad, dicen respeto a la misma unidad de análisis.
Código
# Fusiona el texto de un mismo personaje # en un mismo dialogoag <-aggregate(list(dialogo=dd$pd), by=list(acto=dd$pt,personaje_A=dd$pe,orden=dd$ord), FUN=paste, collapse="\n")
El paso final consiste en eliminar la información innecesaria, como retirar el prefijo “PERSONA-” que hemos empleado como ayuda, y visualizar los resultados.
Para transformar la base de datos en una red de diálogos, necesitamos identificar los personajes que dialogan entre sí. Para ello, creamos una variable que identifique el personaje que responde al interlocutor anterior. En muchas ocasiones, el diálogo se interrumpe por distintas razones, como la entrada de un nuevo personaje, el cambio de escena o el fin de un acto. En estos casos, no podemos considerar que el diálogo anterior continúa. Por lo tanto, necesitamos identificar estos puntos de corte.
La manera más fácil es realizar una lectura rápida de los diálogos para identificar estos puntos. Abajo, creamos en la base de datos una nueva variable llamada “corte” que identifica tales puntos y nos permitirán establecer de forma correcta el sentido del diálogo y sus interlocutores.
Código
# Identifica los puntos de corte en los diálogos# que corresponden a transiciones de escena o# cuándo sale uno o más personajes y se empieza# otro dialogo o se trata de una respuesta al # interlocutor anterior que no obtiene respuesta# y se pasa a un nuevo diálogo con otro personaje. # Por lo tanto, no se puede considerar# como una continuación del dialogo anterior.nc <-c(2,112,177,208,211,217,268,338,365,372,378, 380, 388,399,417,428,439,445, 448,452,463,469,480,494,495,594,599,605,653,661,673,675,745,824,825,834,837,855,864)# Crea una variable corte con valor cero# para todos los diálogosag$corte <-0# En aquellos diálogos que representan# un corte, cambia de cero a uno para# establecer el punto de interrupciónag$corte[nc] <-1
El siguiente paso resulta crucial. Necesitamos identificar el personaje al que se dirige el diálogo. Para ello, creamos una nueva variable en la base de datos llamada “personaje_B” que identificará dicho interlocutor. En este momento, emplearemos los puntos de interrupción o corte en los diálogos para determinar de forma correcta a quién se destina el habla.
Código
# Crea una variable vacía en la base de# datos para almacenar el personaje# que será el receptor de la respuestaag$personaje_B <-NA# Para cada diálogo de la piezafor(i in1:(nrow(ag)-1)){# Si es la introducción del capitulo o de la obra,# se pasa al siguiente diálogoif(ag$personaje_A[i]=="Introducción") next# Si se trata de un corte o fin de escena# se considera como respuesta al personaje# anterior (si no es la introducción de la escena)if(ag$corte[i]==1){# Si el personaje anterior es la introducción# se pasa al siguiente diálogoif(ag$personaje_A[i-1]=="Introducción") next# Atribuye el personaje de destino del diálogo# como en personaje anterior (respuesta final) ag$personaje_B[i] <- ag$personaje_A[i-1]# En caso que no sea un corte de escena }else{ # El personaje de destino del diálogo# es el inmediatamente posterior ag$personaje_B[i] <- ag$personaje_A[i+1] }}# Hay un pasaje en el que Sagra, Carmela y# Trudy se dirigen a Fanny, por eso resulta# necesario corregir el personaje de destinoag$personaje_B[c(339:355,357:365)] <-"FANNY"# La respuesta de Fannyag$personaje_B[c(356)] <-"LAS TRES"# Otra corrección puntualag$personaje_B[c(825)] <-"DIONISIO"# Elimina los espacios en blanco al # final de los nombres de los personajesag$personaje_A <-trimws(ag$personaje_A)ag$personaje_B <-trimws(ag$personaje_B)
Finalmente, creamos la red de diálogos. Ya tenemos una variable con el que habla (personaje_A) y con quién dialoga (personaje_B). A continuación, crearemos una variable para contar cuántas veces hablan entre sí cada par de personajes y sumamos todos. El resultado es una red dirigida de diálogos.
Código
# Separa solo los pares de personaje# en diálogores <- ag[,c("personaje_A","personaje_B")]res <- res[!is.na(res$personaje_B),]# Crea un contador para saber# cuántas veces cada par de personajes# ha dialogadores$veces <-1# Elimina la introducción como personaje# de los dos tipos de redres <- res[res$personaje_A!="Introducción",]res <- res[res$personaje_B!="Introducción",]# Guarda los resultados en un nuevo# objeto para crear una red no# direccionalrea <- res# Suma las veces en que se repiten# los pares de personajes (red dirigida)res <-aggregate(list(freq=res$veces),by=list(personaje_A=res$personaje_A,personaje_B=res$personaje_B),FUN=sum)# ordena en orden decendiente por la frecuencia # en que dialogan res <- res[order(res$freq, decreasing = T),]# Vemos el resultadoreactable(res, resizable = T, wrap = F)
Como podemos observar, la pareja Dionisio-Paula es la que más dialoga en la obra. Viene seguida de las díadas Dionisio-Don Rosario y Paula-El Odioso Señor. Los diálogos entre esos cuatro personajes superan el 60% de todas las interacciones en la pieza. Pero ya nos vamos adelantando con el análisis.
Concentrémonos ahora en la creación de la red no dirigida. Se trata de la última etapa en la preparación de los datos. Como ya hemos mencionado, también nos interesa saber cuántas veces han dialogado dos personajes sin importar quién es el que inicia la conversación.
Código
# Carga el paquete necesario para lidiar con# grafoslibrary(igraph)# Uniformiza los valores duplicados# (Dionisio-Paula y Paula-Dionisio, # por ejemplo, se convierten todos # en Dionisio-Paula)# 1) Crea un grafo NO DIRECCIONAL a partir de # la red de personajesg <-graph_from_data_frame( rea[,c("personaje_A","personaje_B")], directed=FALSE)# 2) Simplifica la estructura para hacer con que# todos los valores estén en una sola dirección# pero sin remover los repetidos, pues queremos# contarlosg <-simplify(g, remove.multiple = F, remove.loops = F)# Convierte la red en una base de datosax <- igraph::as_data_frame(g)# Cuenta cuántas veces se repite cada parax$freq <-1# Suma las veces en que se repiten# los pares de personajes (red no dirigida)aa <-aggregate(list(freq=ax$freq),by=list(personaje_A=ax$from,personaje_B=ax$to),FUN=sum)# Ordena los resultados en orden descendenteaa <- aa[order(aa$freq, decreasing = T),]# Vemos los resultadosreactable(aa, resizable = T, wrap = F)
Finalmente, estandarizamos los nombres de las bases de datos y las guardamos en un archivo de R.
Código
# Estandariza los nombres de las bases # de datos# Tres sombreros de copa - dialogostsc_d <- ag # Tres sombreros de copa - red de personajes (no dirigida)tsc_rn <- aa # Tres sombreros de copa - red de personajes (dirigida)tsc_rd <- res # Guarda los resultados# Elegid una ubicación en vuestro# ordenador donde podáis rescatar# los datos luego:# "C:/FiloR/Tres_sombreros_de_copa.RData", # por ejemplosave(tsc_d, tsc_rd, tsc_rd, file="textos/Tres_sombreros_de_copa.RData")
Ejemplo 1: Don Quijote
Ahora repetiremos la preparación de una novela, pero ahora utilizaremos “El Quijote”.
Como en el caso de “La Regenta”, bajaremos el texto de la obra desde el Proyecto Gutenberg. El código del Quijote es 2000.
Código
# Carga los paquetes# empleadoslibrary(stringi)library(readr)library(gutenbergr)library(reactable)# Baja el texto del Quijote# cuyo id es igual 2000re <-gutenberg_download(gutenberg_id =2000, verbose =FALSE)
Luego, arreglaremos los párrafos y convertiremos cada párrafo en una observación.
Código
# Convierte el texto en un solo stringre <-paste0(re$text, collapse ="\n")# Corrige los párrafos para que no tengan# un salto de línea al final de cada fraserx <-stri_replace_all_regex( re,"(\\S|\\p{L})(\n)(\\S{1}|\\p{L})","$1 $3")# convierte cada parrafo en una# observacionre <-read_lines(rx)
Acto seguido, identificamos el prólogo, las partes y los capítulos.
Código
# Encuentra el prologopr <-which(stri_detect_regex(re, "^PRÓLOGO")==TRUE) # capítulos# Encuentra las partespt <-which(stri_detect_regex(re, "^(Primera|Segunda)")==TRUE) # capítulospt <- pt[c(2,4)]# Encuentra los capituloscap <-which(stri_detect_regex(re, "^Capítulo")==TRUE) # capítulos
A continuación, extraemos los textos y empezamos a preparar la base de datos.
Código
# Extrae los textos de las partes# y capitulostx <- re[pt] # partes - textoscx <- re[cap] # capítulos - textos# Hacemos algo parecido con el prólogo. # Repetimos "Prólogo" desde la primera # vez que aparece (pro) hasta el párrafo# anterior a la primera parte.pro <-rep("00 - Prólogo", length(1:(pt[1]-1)))# Para las partes# Encuentra el tamaño en párrafos# de cada partelen <-diff(c(pt, length(re)+1))# Repite la descripción o el nombre# de cada parte para identificar# cada párrafota <-sapply(1:(length(tx)), function(i){rep(tx[i], len[i]) }, simplify =TRUE)ta <-unlist(ta)# Para los capítulos# Encuentra el tamaño en párrafos# de cada capítulolen <-diff(c(cap, length(re)+1))# Repite la descripción o el nombre# de cada capítulo para identificar# cada párrafoca <-sapply(1:(length(cx)), function(i){rep(cx[i], len[i]) }, simplify =TRUE)ca <-unlist(ca)# Combina los vectores de prólogo# y partepta <-c(pro,ta)# Genera una base de datos con las# informaciones completas de# identificación de las partes,# y el texto.dx <-data.frame(parte = pta, texto = re)
Añadimos la información de capítulos, transformando los números romanos en números arábigos para facilitar la ordenación de los capítulos según la secuencia original. También separamos el número del capítulo de su descripción y eliminamos los párrafos con espacios en blanco.
Código
# Aniade la información de los# capitulosdx$capitulo[cap[1]:nrow(dx)] <- ca# Reemplaza los nombres del capitulo# primero a romano para uniformizar# el numero de capitulosdx$capitulo <-gsub("Capítulo primero", "Capítulo I", dx$capitulo)dx$capitulo <-gsub("Capítulo Primero", "Capítulo I", dx$capitulo)# Separa el numero de la descripcion# del capitulott <-stri_split_regex(dx$capitulo, "\\.\\s{1}", 2, simplify = T)dx$capitulo <- tt[,1]dx$cap_desc <- tt[,2]# Añade un capitulo 00 para el prologo # de la segunda partedx$capitulo[c(4673:4755)] <-"00"dx$cap_desc[c(4673:4755)] <-""# Elimina los parrafos con# espacios en blancodx <- dx[dx$texto !="", ]dx <- dx[dx$texto !=" ", ]# Elimina la palabra capitulodx$capitulo <-gsub("Capítulo","",dx$capitulo)dx$capitulo <-trimws(dx$capitulo)# Convierte los textos de identificación# de los capítulos en números romanos# y luego los convierte en numéricodx$roman <-as.roman(dx$capitulo)dx$roman <-as.numeric(dx$roman)# Asigna los valores numéricos a los capítulosdx$capitulo[!is.na(dx$roman)] <- dx$roman[!is.na(dx$roman)]# Añade un 0 (cero) para los capítulos menores# a 10 y 00 para el prologo y la introduccion# de cada parte.dx$capitulo[is.na(dx$roman)] <-"00"dx$cap_desc[is.na(dx$roman)] <-""dx$capitulo[nchar(dx$capitulo)==1] <-paste0("0", dx$capitulo[nchar(dx$capitulo)==1])
Finalmente, unificamos el texto de cada capítulo como una única observación, ordenamos la base de datos por parte y capítulo, y creamos una variable de orden para facilitar la reordenación del texto. Por último, guardamos los resultados en un archivo RData.
Código
# Unifica el texto de cada capítulo# como una unica observaciondq <-aggregate(texto~parte+capitulo+cap_desc, dx, FUN=paste, collapse="\n")# Ordena según parte y capitulo# y crea un orden para facilitar# la reordenacion del textodq <- dq[order(dq$parte, dq$capitulo),]dq$orden <-1:nrow(dq)# Examina el resultadoreactable(dq, wrap = F,resizable=T, sortable=T)
Código
# Guarda los resultadossave(dq, file="../textos/Quijote.RData")
Ejemplo 2: El sí de las niñas
Reproducimos ahora la misma lógica para la obra “El sí de las niñas” de Leandro Fernández de Moratín. Como en el caso de “Tres sombreros de copa”, la obra se divide en actos y escenas. El objetivo es crear una base de datos con los diálogos, identificar los puntos de corte y crear las redes de diálogos dirigidas y no dirigidas.
No obstante, ahora hay una diferencia crucial. Entre el código anterior y el siguiente está la aplicación de la IA para generar el código. Cómo veréis, el código es distinto, puesto que la formatación y la estructura cambian ligeramente. He subido el texto integral en chatGPT y empleado el siguiente prompt para que me generara el código en R:
Necesito que generes un script en R que estructure el texto de la siguiente manera: Acto, Escena, Personaje, texto. De ese modo, por ejemplo, “Acto I”,“Escena I”, “Don Diego”, “¿No han venido todavía?”˝Acto I”,“Escena I”,“Simón”,“No, señor.” y así sucesivamente. Genere un script en lenguaje R que permita dividir la información en formato csv, como en los ejemplos que le di. No olvides de identificar de forma adecuada los actos, las escenas y los personajes. Utilice el paquete stringi para la manipulación de los textos.
El resultado ha sido parecido al siguiente, la diferencia es que lo he simplificado luego para que fuera más sencillo:
Código
# Cargar librerías necesariaslibrary(readr)library(dplyr)library(stringi)# Lee el archivo de la piezatexto <-read_lines("https://raw.githubusercontent.com/rodrodr/filoR/refs/heads/master/si_ninas.txt")# Crea una base de datos para# almacenar los diálogossi <-data.frame(Acto=character(), Escena=character(), Personaje=character(), Texto=character(), stringsAsFactors=FALSE)for (linea in texto) { linea <-stri_trim_both(linea)if (linea =="") next# FINif (stri_detect_regex(linea, "^\\s*FIN\\s*$")) break# Detectar ACTO aunque no esté solo en la líneaif (stri_detect_regex(linea, "Acto\\s+([IVXLCDM]+)")) { numero_acto <-stri_match_first_regex(linea, "Acto\\s+([IVXLCDM]+)")[,2] acto <-paste0("Acto ", numero_acto)next }# Escena (sí suele estar sola)if (stri_detect_regex(linea, "^\\s*Escena\\s+([IVXLCDM]+)\\s*$")) { numero_escena <-stri_match_first_regex(linea, "^\\s*Escena\\s+([IVXLCDM]+)\\s*$")[,2] escena <-paste0("Escena ", numero_escena)next }# Diálogoif (stri_detect_regex(linea, "^([A-ZÁÉÍÓÚÑÜ ]+)(?:\\.|\\.-)\\s+(.*)$")) { partes <-stri_match_first_regex(linea, "^([A-ZÁÉÍÓÚÑÜ ]+)(?:\\.|\\.-)\\s+(.*)$") personaje <-stri_trans_totitle(stri_trim_both(partes[,2])) texto_dialogo <-stri_trim_both(partes[,3]) si <-add_row(si, Acto = acto, Escena = escena,Personaje = personaje, Texto = texto_dialogo) }}si$orden <-1:nrow(si)si$Personaje[si$Personaje=="Diego"] <-"Don Diego"si$Personaje[si$Personaje=="D Carlos"] <-"Don Carlos"# Vemos el resultadoreactable(si, resizable = T, wrap = T)
Ahora repetimos lo que hicimos con “Tres sombreros de copa” para crear redes de diálogo. Repetimos el mismo código, pero ahora con la nueva base de diálogos de “El sí de las niñas”.
De datos a redes de diálogos
Empezamos por la identificación de los puntos de corte en los diálogos.
Código
# Identifica los puntos de corte en los diálogos# que corresponden a transiciones de escena o# cuándo sale uno o más personajes y se empieza# otro dialogo o se trata de una respuesta al # interlocutor anterior que no obtiene respuesta# y se pasa a un nuevo diálogo con otro personaje. # Por lo tanto, no se puede considerar# como una continuación del dialogo anterior.nc <-c(78,80,83,89,91,93,95,104,105,154,161,171,206,250,251,264,279,295,305,307,309,323,325,326,329,330,332,335,345,347,357,392,394,395,397,405,409,423,521,536,548,571,600,615,624,639,644,645,660,724,736,779,817,832,844,847)# Crea una variable corte con valor cero# para todos los diálogossi$corte <-0# En aquellos diálogos que representan# un corte, cambia de cero a uno para# establecer el punto de interrupciónsi$corte[nc] <-1
El siguiente paso resulta crucial. Necesitamos identificar el personaje al que se dirige el diálogo. Para ello, creamos una nueva variable en la base de datos llamada “personaje_B” que identificará dicho interlocutor. En este momento, emplearemos los puntos de interrupción o corte en los diálogos para determinar de forma correcta a quién se destina el habla.
Código
# Crea una variable vacía en la base de# datos para almacenar el personaje# que será el receptor de la respuestasi$personaje_A <- si$Personajesi$personaje_B <-NA# Para cada diálogo de la piezafor(i in1:(nrow(si)-1)){# Si es la introducción del capitulo o de la obra,# se pasa al siguiente diálogoif(si$personaje_A[i]=="Introducción") next# Si se trata de un corte o fin de escena# se considera como respuesta al personaje# anterior (si no es la introducción de la escena)if(si$corte[i]==1){# Si el personaje anterior es la introducción# se pasa al siguiente diálogoif(si$personaje_A[i-1]=="Introducción") next# Atribuye el personaje de destino del diálogo# como en personaje anterior (respuesta final) si$personaje_B[i] <- si$personaje_A[i-1]# En caso que no sea un corte de escena }else{ # El personaje de destino del diálogo# es el inmediatamente posterior si$personaje_B[i] <- si$personaje_A[i+1] }}# Elimina los espacios en blanco al # final de los nombres de los personajessi$personaje_A <-trimws(si$personaje_A)si$personaje_B <-trimws(si$personaje_B)
Finalmente, creamos la red de diálogos. Ya tenemos una variable con el que habla (personaje_A) y con quién dialoga (personaje_B). A continuación, crearemos una variable para contar cuántas veces hablan entre sí cada par de personajes y sumamos todos. El resultado es una red dirigida de diálogos.
Código
# Separa solo los pares de personaje# en diálogores <- si[,c("personaje_A","personaje_B")]res <- res[!is.na(res$personaje_B),]# Crea un contador para saber# cuántas veces cada par de personajes# ha dialogadores$veces <-1# Elimina la introducción como personaje# de los dos tipos de redres <- res[res$personaje_A!="Introducción",]res <- res[res$personaje_B!="Introducción",]# Guarda los resultados en un nuevo# objeto para crear una red no# direccionalrea <- res# Suma las veces en que se repiten# los pares de personajes (red dirigida)res <-aggregate(list(freq=res$veces),by=list(personaje_A=res$personaje_A,personaje_B=res$personaje_B),FUN=sum)# ordena en orden decendiente por la frecuencia # en que dialogan res <- res[order(res$freq, decreasing = T),]# Vemos el resultadoreactable(res, resizable = T, wrap = F)
Como podemos observar, la pareja Doña Irene-Don Diego es la que más dialoga en la obra. Viene seguida de las díadas Simón-Don Diego y Rita-Doña Franscisca.
Concentrémonos ahora en la creación de la red no dirigida. Se trata de la última etapa en la preparación de los datos. Como ya hemos mencionado, también nos interesa saber cuántas veces han dialogado dos personajes sin importar quién es el que inicia la conversación.
Código
# Carga el paquete necesario para lidiar con# grafoslibrary(igraph)# Uniformiza los valores duplicados# 1) Crea un grafo NO DIRECCIONAL a partir de # la red de personajesg <-graph_from_data_frame( rea[,c("personaje_A","personaje_B")], directed=FALSE)# 2) Simplifica la estructura para hacer con que# todos los valores estén en una sola dirección# pero sin remover los repetidos, pues queremos# contarlosg <-simplify(g, remove.multiple = F, remove.loops = F)# Convierte la red en una base de datosax <- igraph::as_data_frame(g)# Cuenta cuántas veces se repite cada parax$freq <-1# Suma las veces en que se repiten# los pares de personajes (red no dirigida)aa <-aggregate(list(freq=ax$freq),by=list(personaje_A=ax$from,personaje_B=ax$to),FUN=sum)# Ordena los resultados en orden descendenteaa <- aa[order(aa$freq, decreasing = T),]# Vemos los resultadosreactable(aa, resizable = T, wrap = F)
Finalmente, estandarizamos los nombres de las bases de datos y las guardamos en un archivo de R.
Código
# Estandariza los nombres de las bases # de datos# Tres sombreros de copa - dialogossi_d <- si # Tres sombreros de copa - red de personajes (no dirigida)si_rn <- aa # Tres sombreros de copa - red de personajes (dirigida)si_rd <- res # Guarda los resultados# Elegid una ubicación en vuestro# ordenador donde podáis rescatar# los datos luego:# "C:/FiloR/si_ninas.RData", # por ejemplosave(si_d, si_rd, si_rd, file="../textos/si_ninas.RData")